package cn.caplike.demo.nio._2021.selector.another_demo;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * Description: 服务端.
 * <br>Details: 使用一个通道或者多个通道都行. 如果是多个通道, 那不同的客户端可能会连接到某个通道对应的端口号的 channel 上.
 * <p>服务端只会有一个线程, 不像传统的网络编程: 一个连接服务器断就会起一个线程, 与这个连接进行双向的数据通信.
 * <p>一个客户端向服务端发起信息后, 服务端能把这个数据分发给其他所有的客户端, 服务端需要保持客户端的什么信息: 至少要保存一个连接.
 * <p>与传统IO的最大区别是: 服务器端只有一个线程</p>
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2022-01-22 14:00
 */
public class NioServer {

    private static final Map<String, SocketChannel> clientMap = new HashMap<>();

    public static void main(String[] args) throws IOException {
        // ~ 服务器端要保存客户端的信息才能将信息从 A 转到 B: SocketChannel
        final ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 非阻塞

        // ServerSocket: 服务器端的 Socket 类型
        final ServerSocket serverSocket = serverSocketChannel.socket(); // 获得服务器端的 serverSocket 对象 - A
        serverSocket.bind(new InetSocketAddress(8899));
        // ---------------------------------------------------------------------------------------------------------------------------------

        final Selector selector = Selector.open();
        // ~ 将 ServerSocketChannel 注册到 Selector. Selector 会关注多个 ServerSocketChannel 对象
        // 一个 Selector 对象可以被注册多个 Channel 对象, Selector 的 select 方法会阻塞直到 "Channel 发生 Selector 感兴趣的事件之后"
        //                  +---------+
        //           +---<--| CHANNEL |
        //           |      +---------+
        //           |
        // +---------++     +---------+
        // | SELECTOR +--<--| CHANNEL |
        // +---------++     +---------+
        //           |
        //           |      +---------+
        //           +---<--| CHANNEL |
        //                  +---------+
        serverSocketChannel.register(selector, /* The interest set for the resulting key */ SelectionKey.OP_ACCEPT); // idx.1: (因为一开始没有客户端连接) 服务器端会关注连接事件

        // noinspection InfiniteLoopStatement
        while (true) {
            try {
                final int num = selector.select(); // 阻塞: 直到其 InterestSets 事件发生, 返回所关注的事件的数量
                final Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回之前所关注的所有的感兴趣的事件的 SelectionKey 的集合
                // 不仅可以通过 SelectionKey 获取到对应事件的集合, 还可以反之获取到与之关联的发生事件的 Channel 对象

                selectionKeys.forEach(selectionKey -> {
                    final SocketChannel client; // 表示对应客户端的 channel 对象
                    try {
                        // 判断 selectionKey 类型
                        if (selectionKey.isAcceptable()) { // @see idx.1
                            // 客户端向服务端发起了一个连接: 通过当前 SelectionKey 获取到与之关联的 ServerSocketChannel 对象, 开始调用其 accept 方法, 再将服务端与客户端连接的通道对象注册到 Selector 上去
                            // 获取到当前的事件发生在哪一个通道上, 为什么是 ServerSocketChannel? -> @see idx.1: 其实就是 A
                            final ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                            client = server.accept(); // 表示服务器端真正的接受了客户端连接, 它会返回 SocketChannel (而不是 ServerSocketChannel) 对象: 这个时候连接真正被接受, 服务端就会通过这个 SocketChannel 与客户端通信.
                            // 这个时候 ServerSocketChannel 已经用不上了, 其作用就是获取与客户端建立连接的 SocketChannel 对象.

                            // ~ 也注册到 Selector 上
                            client.configureBlocking(false);
                            client.register(selector, SelectionKey.OP_READ); // idx.2: 此时 ServerSocketChannel 和 SocketChannel 两个对象都注册到了唯一的 Selector 上了
                            // ServerSocketChannel 关注连接事件; SocketChannel 关心读取事件

                            // 客户端已经和服务端建立好连接了, 且服务端已经把对应的 Channel 注册到 Selector 上了
                            // 一定要将客户端信息保存到服务器上, 服务器才能进行消息的分发
                            final String key = "[" + UUID.randomUUID() + "]";
                            clientMap.put(key, client);
                        } else if (selectionKey.isReadable()) {
                            client = (SocketChannel) selectionKey.channel(); // @see idx.2

                            final ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                            final int count = client.read(readBuffer);
                            if (count > 0) {
                                // 写到所有与之连接的客户端
                                readBuffer.flip();

                                final Charset charset = StandardCharsets.UTF_8;
                                final String receivedMessage = String.valueOf(charset.decode(readBuffer).array());
                                System.out.println(client + ": " + receivedMessage);

                                String senderKey = null;
                                for (final Map.Entry<String, SocketChannel> entry : clientMap.entrySet()) {
                                    if (client == entry.getValue()) {
                                        senderKey = entry.getKey();
                                        break;
                                    }
                                }

                                // 向客户端发送消息
                                for (final Map.Entry<String, SocketChannel> entry : clientMap.entrySet()) {
                                    SocketChannel value = entry.getValue();

                                    final ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
                                    writeBuffer.put((senderKey + ": " + receivedMessage).getBytes(charset)); // 读
                                    writeBuffer.flip();

                                    value.write(writeBuffer); // 写
                                }
                            }
                        } else System.err.println("Unreachable event: " + selectionKey);

                        selectionKeys.remove(selectionKey); // ! 每次处理完之后一定将 SelectionKey 从集合中删掉
                        // @formatter:off
                    } catch (Exception ex) { ex.printStackTrace(); }
                });
            } catch (Exception ex) { ex.printStackTrace(); }
        }
    }
}
